'''
Code:
Origin Code is from https://github.com/slightlynybbled/tk_tools/blob/master/tk_tools/canvas.py
Edited by Hou Zhanyi

'''
from decimal import Decimal
from tkinter import ttk
import tkinter as tk


class Graph(ttk.Frame):
    """
    Tkinter native graph (pretty basic, but doesn't require heavy install).::
        graph = tk_tools.Graph(
            parent=root,
            x_min=-1.0,
            x_max=1.0,
            y_min=0.0,
            y_max=2.0,
            x_tick=0.2,
            y_tick=0.2,
            width=500,
            height=400
        )
        graph.grid(row=0, column=0)
        # create an initial line
        line_0 = [(x/10, x/10) for x in range(10)]
        graph.plot_line(line_0)
    :param parent: the parent frame
    :param x_min: the x minimum
    :param x_max: the x maximum
    :param y_min: the y minimum
    :param y_max: the y maximum
    :param x_tick: the 'tick' on the x-axis
    :param y_tick: the 'tick' on the y-axis
    :param options: additional valid tkinter.canvas options
    """

    def __init__(self, parent, x_min=0.0, x_max=1.0,
                 y_min=0.0, y_max=1.0,
                 x_tick=0.2, y_tick=0.2,
                 **options):
        self._parent = parent
        super().__init__(self._parent, **options)

        self.canvas = tk.Canvas(self)
        self.canvas.pack(fill=tk.BOTH, expand=1)
        self.canvas.bind('<Configure>', self.on_resize)
        self.w = float(self.canvas.config('width')[4])
        self.h = float(self.canvas.config('height')[4])
        self.x_min = x_min
        self.x_max = x_max
        self.x_tick = x_tick
        self.y_min = y_min
        self.y_max = y_max
        self.y_tick = y_tick
        self.px_x = (self.w - 100) / ((x_max - x_min) / x_tick)
        self.px_y = (self.h - 100) / ((y_max - y_min) / y_tick)

        self.draw_axes()

    def on_resize(self, event):
        self.w = self.canvas.winfo_width()
        self.h = self.canvas.winfo_height()
        self.px_x = (self.w - 100) / ((self.x_max - self.x_min) / self.x_tick)
        self.px_y = (self.h - 100) / ((self.y_max - self.y_min) / self.y_tick)
        pass

    def draw_axes(self):
        """
        Removes all existing series and re-draws the axes.
        :return: None
        删除当前画布全部对象，并且重绘坐标轴。
        返回值为None。
        """
        self.canvas.delete('all')
        rect = 50, 50, self.w - 50, self.h - 50

        self.canvas.create_rectangle(rect, outline="black")

        for x in self.frange(0, self.x_max - self.x_min + 1, self.x_tick):
            # print(type(self.x_min), type(x))
            value = Decimal(self.x_min + x)  # 嵌套一层float(),因为可能传入np.float型，需要进行类型转换。
            # print(value, type(value), type(self.x_min), type(self.x_max))
            if self.x_min <= value <= self.x_max:
                x_step = (self.px_x * x) / self.x_tick
                coord = 50 + x_step, self.h - 50, 50 + x_step, self.h - 45
                self.canvas.create_line(coord, fill="black")
                coord = 50 + x_step, self.h - 40

                label = round(Decimal(self.x_min + x), 1)
                self.canvas.create_text(coord, fill="black", text=label)

        for y in self.frange(0, self.y_max - self.y_min + 1, self.y_tick):
            value = Decimal(self.y_max - y)

            if self.y_min <= value <= self.y_max:
                y_step = (self.px_y * y) / self.y_tick
                coord = 45, 50 + y_step, 50, 50 + y_step
                self.canvas.create_line(coord, fill="black")
                coord = 35, 50 + y_step

                label = round(value, 1)
                self.canvas.create_text(coord, fill="black", text=label)

    def plot_point(self, x: float, y: float, visible=True, color='black', size=5):
        """
        Places a single point on the grid
        :param x: the x coordinate（点的x坐标）
        :param y: the y coordinate（点的y坐标）
        :param visible: True if the individual point should be visible (如果为True,代表这个点是可见的。反之不可见。)
        :param color: the color of the point(点的颜色)
        :param size: the point size in pixels（点的大小，单位为像素。）
        :return: The absolute coordinates as a tuple(返回值为点的绝对坐标，以tuple的形式)
        """
        xp = (self.px_x * (x - self.x_min)) / self.x_tick
        yp = (self.px_y * (self.y_max - y)) / self.y_tick
        coord = 50 + xp, 50 + yp

        if visible:
            # divide down to an appropriate size
            size = int(size / 2) if int(size / 2) > 1 else 1
            x, y = coord

            self.canvas.create_oval(
                x - size, y - size,
                x + size, y + size,
                fill=color
            )

        return coord

    def plot_line(self, points: list, color='black', point_visibility=False):
        """
        Plot a line of points
        :param points: a list of tuples, each tuple containing an (x, y) point
        :param color: the color of the line
        :param point_visibility: True if the points \
        should be individually visible
        :return: None
        """
        last_point = ()
        for point in points:
            this_point = self.plot_point(point[0], point[1],
                                         color=color, visible=point_visibility)

            if last_point:
                self.canvas.create_line(last_point + this_point, fill=color)
            last_point = this_point
            # print last_point

    def plot(self, x: list, y: list, color='black', markercolor='', markersize=5, linecolor='', showmarker=True,
             showline=True, xyautofit=True) -> None:
        '''
        x:list,such as [1,2,3]
        y:list,such as [1,2,3]
        color:str.the color for line and dot
        linecolor:str.the color for line.If unfilled,it will be set as the same as color
        markercolor:str.the color for marker.If unfilled, it will be set the same as color
        show_line:bool.If True, the graph will show line between markers
        show_marker: bool.If True,the graph will show markers.
        ATTENTION:If neither of show_marker and show_line is True, this function will not plot anything.

        x:列表，比如[1,2,3]
        y:列表，比如[1,2,3]
        color:字符串，比如'black'。决定线条和标记的颜色。
        linecolor:字符串。决定线条颜色
        markercolor:字符串，决定标记的颜色
        showline:bool型，默认为True。决定线段颜色。
        showmarker:bool型，默认为True。决定点的颜色。
        xyautofit:bool型。默认为True，决定是否自动适应坐标轴。
        注意：
        1、如果show_line和show_marker都是False的话，将不会显示任何内容。
        2、autofit开启的时候，将清除之前绘制的图像。若希望在同一坐标下绘制多个图像，请将autofit置为False。并且，在首次绘图的时候选定
        合适的画布大小。
        3、散点的形状不可改变，只有圆形一种，请谅解。
        '''
        if xyautofit:
            self.x_min = float(min(x))
            self.y_min = float(min(y))
            self.x_max = float(max(x))
            self.y_max = float(max(y))
            self.x_tick = (self.x_max - self.x_min) / 5
            self.y_tick = (self.y_max - self.y_min) / 5
            print(self.w,self.h)
            self.px_x = (self.w - 100) / ((self.x_max - self.x_min) / self.x_tick)
            self.px_y = (self.h - 100) / ((self.y_max - self.y_min) / self.y_tick)
            self.draw_axes()

        if markercolor == '':
            markercolor = color
        if linecolor == '':
            linecolor = color

        last_point = ()
        for point in zip(x, y):
            this_point = self.plot_point(point[0], point[1],
                                         color=markercolor, visible=showmarker, size=markersize)
            if last_point and showline:
                self.canvas.create_line(last_point + this_point, fill=linecolor)
            last_point = this_point

    @staticmethod
    def frange(start, stop, step, digits_to_round=3):
        """
        Works like range for doubles
        :param start: starting value
        :param stop: ending value
        :param step: the increment_value
        :param digits_to_round: the digits to which to round \
        (makes floating-point numbers much easier to work with)
        :return: generator
        """
        while start < stop:
            yield round(start, digits_to_round)
            start += step


if __name__ == '__main__':
    '''
    点击按钮即可绘图。
    发生缩放时，图表不会自动刷新。缩放完成后再次点击绘图，即可缩放图像大小。
    '''


    def plotfunc():
        global graph
        graph.draw_axes()
        graph.plot(x=[1, 2, 3], y=[0.4, 0.8, 0.6], showmarker=True)


    root = tk.Tk()
    graph = Graph(parent=root,
                  x_min=0,
                  x_max=3.0,
                  y_min=0.0,
                  y_max=2.0,
                  x_tick=0.2,
                  y_tick=0.2,
                  width=500,
                  height=600)

    graph.pack(fill=tk.BOTH, expand=1)

    b = ttk.Button(root, text='绘图', command=plotfunc)
    b.pack()
    root.mainloop()
